package com.zws.cucumber.plugin.procedure;

import com.zws.cucumber.util.colorfullog.ZwsColorfulLogger;
import com.zws.cucumber.util.colorfullog.ZwsPrintTableHelper;
import com.zws.cucumber.util.colorfullog.table.ZwsFixedPrefixRender;
import com.zws.cucumber.var.ZwsVar;
import io.cucumber.core.eventbus.EventBus;
import io.cucumber.core.runner.CoreRunnerTestCaseHelper;
import io.cucumber.plugin.event.TestCase;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.collections4.MapUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.slf4j.Logger;
import org.springframework.util.ReflectionUtils;

import java.lang.reflect.Method;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.stream.Collectors;

import static com.zws.cucumber.util.colorfullog.ZwsColorfulLogHelper.*;

/**
 * author: zws
 */
public class ZwsProcedureUtil {

    private static final Logger logger = ZwsColorfulLogger.of(ZwsProcedureUtil.class);

    public static ConcurrentMap<String, TestCase> procedureMap = new ConcurrentHashMap<>();
    public static ConcurrentMap<String, List<String>> argumentsMap = new ConcurrentHashMap<>();

    private static final int MaxStackDepth = 8;
    private static ZwsProcedureStackFrame[] stack = new ZwsProcedureStackFrame[8];
    private static Object lastResult;

    private static final Pair[] SlashTags = new Pair[]{
            Pair.of("<c>", "</c>"),
            Pair.of("<y>", "</y>"),
            Pair.of("<b>", "</b>"),
            Pair.of("<p>", "</p>"),
    };

    private static Pair slashTag(int i) {
        return SlashTags[(i - 1) % SlashTags.length];
    }

    private static String renderSlashTag(int i, String s) {
        return slashTag(i).getLeft() + s + slashTag(i).getRight();
    }

    private static String prefixSlash(int i) {
        if (i == 1) {
            return slashTag(1).getLeft() + "/" + slashTag(1).getRight();
        }
        return prefixSlash(i - 1) + slashTag(i).getLeft() + "/" + slashTag(i).getRight();
    }

    private static String prefix(int i) {
        return prefixSlash(i) + slashTag(i).getLeft() + i + slashTag(i).getRight() + " ";
    }

    private static String[] Prefixed = new String[] {
            "",
            prefix(1),
            prefix(2),
            prefix(3),
            prefix(4),
            prefix(5),
            prefix(6),
            prefix(7),
            prefix(8),
    };

    private static String[] Separators = new String[] {
            "",
            "\n" + prefix(1) + slashTag(1).getLeft() + fill('/', 125) + slashTag(1).getRight(),
            "\n" + prefix(2) + slashTag(2).getLeft() + fill('/', 124) + slashTag(2).getRight(),
            "\n" + prefix(3) + slashTag(3).getLeft() + fill('/', 123) + slashTag(3).getRight(),
            "\n" + prefix(4) + slashTag(4).getLeft() + fill('/', 122) + slashTag(4).getRight(),
            "\n" + prefix(5) + slashTag(5).getLeft() + fill('/', 121) + slashTag(5).getRight(),
            "\n" + prefix(6) + slashTag(6).getLeft() + fill('/', 120) + slashTag(6).getRight(),
            "\n" + prefix(7) + slashTag(7).getLeft() + fill('/', 119) + slashTag(7).getRight(),
            "\n" + prefix(8) + slashTag(8).getLeft() + fill('/', 118) + slashTag(8).getRight(),
    };

    private static String[] HeaderPatterns = new String[] {
            "",
            Separators[1] + "\n" + prefix(1) + renderSlashTag(1, "S: {}") + "\n{}",
            Separators[2] + "\n" + prefix(2) + renderSlashTag(2, "S: {}") + "\n{}",
            Separators[3] + "\n" + prefix(3) + renderSlashTag(3, "S: {}") + "\n{}",
            Separators[4] + "\n" + prefix(4) + renderSlashTag(4, "S: {}") + "\n{}",
            Separators[5] + "\n" + prefix(5) + renderSlashTag(5, "S: {}") + "\n{}",
            Separators[6] + "\n" + prefix(6) + renderSlashTag(6, "S: {}") + "\n{}",
            Separators[7] + "\n" + prefix(7) + renderSlashTag(7, "S: {}") + "\n{}",
            Separators[8] + "\n" + prefix(8) + renderSlashTag(8, "S: {}") + "\n{}",
    };

    private static String[] FooterPatterns = new String[] {
            "",
            "\n" + prefix(1) + slashTag(1).getLeft() + "E: {}" + slashTag(1).getRight() + Separators[1],
            "\n" + prefix(2) + slashTag(2).getLeft() + "E: {}" + slashTag(2).getRight() + Separators[2],
            "\n" + prefix(3) + slashTag(3).getLeft() + "E: {}" + slashTag(3).getRight() + Separators[3],
            "\n" + prefix(4) + slashTag(4).getLeft() + "E: {}" + slashTag(4).getRight() + Separators[4],
            "\n" + prefix(5) + slashTag(5).getLeft() + "E: {}" + slashTag(5).getRight() + Separators[5],
            "\n" + prefix(6) + slashTag(6).getLeft() + "E: {}" + slashTag(6).getRight() + Separators[6],
            "\n" + prefix(7) + slashTag(7).getLeft() + "E: {}" + slashTag(7).getRight() + Separators[7],
            "\n" + prefix(8) + slashTag(8).getLeft() + "E: {}" + slashTag(8).getRight() + Separators[8],
    };

    /**
     * Make sure that just invoking procedure in single level
     */
//    private static boolean isInvoking = false;
    private static int invokingLevel = 0;

    public static int invokingLevel() {
        return invokingLevel;
    }

    /*
     * Parse
     */

    public static Pair<String, List<String>> parseProcedureSignature(String scenario) {
        String procedureName = parseProcedureName(scenario);
        String argsPartOfScenario = scenario.substring(scenario.indexOf(procedureName) + procedureName.length());
        if (argsPartOfScenario.trim().isEmpty()) {
            return Pair.of(procedureName, new ArrayList<>());
        }
        List<String> procedureArgumentList = parseProcedureArgumentList(argsPartOfScenario);
        return Pair.of(procedureName, procedureArgumentList);
    }

    private static String parseProcedureName(String scenario) {
        int i = 0;
        while (Character.isWhitespace(scenario.charAt(i))) { i++; }
        StringBuilder sb = new StringBuilder();
        if (Character.isJavaIdentifierStart(scenario.charAt(i))) {
            sb.append(scenario.charAt(i++));
            while (i < scenario.length() && Character.isJavaIdentifierPart(scenario.charAt(i))) {
                sb.append(scenario.charAt(i++));
            }
        } else {
            throw new IllegalArgumentException("Invalid Procedure Signature: " + scenario);
        }
        return sb.toString();
    }

    private static List<String> parseProcedureArgumentList(String argsPartOfScenario) {
        int i = 0;
        CharSequence cs = argsPartOfScenario;
        List<String> argList = new ArrayList<>();
        while (Character.isWhitespace(cs.charAt(i))) { i++; }
        if ('(' != cs.charAt(i++)) { throw new IllegalArgumentException("Invalid Procedure Signature: " + argsPartOfScenario); }
        while (Character.isWhitespace(cs.charAt(i))) { i++; }
        if (Character.isJavaIdentifierStart(cs.charAt(i))) {
            do {
                if (',' == cs.charAt(i)) i++;
                while (Character.isWhitespace(cs.charAt(i))) { i++; }
                StringBuilder sb = new StringBuilder();
                if (Character.isJavaIdentifierStart(cs.charAt(i))) {
                    sb.append(cs.charAt(i++));
                    while (Character.isJavaIdentifierPart(cs.charAt(i))) {
                        sb.append(cs.charAt(i++));
                    }
                }
                argList.add(sb.toString());
                while (Character.isWhitespace(cs.charAt(i))) { i++; }
            } while (',' == cs.charAt(i));
        }
        if (')' != cs.charAt(i++)) { throw new IllegalArgumentException("Invalid Procedure Signature: " + argsPartOfScenario); }
        return argList;
    }

    /*
     * Invoke
     */

    public static void checkArgumentNames(String procedureName, Set<String> passed) {
        List<String> defined = argumentsMap.get(procedureName);
        if (defined == null) {
            throw new IllegalArgumentException("No Such Procedure: " + procedureName);
        }
        if (passed.size() > defined.size()) {
            throw new IllegalArgumentException(
                "Number of passed arguments is larger than defined, "
                    + "procedure: " + procedureName + ", "
                    + "diff: passed" + CollectionUtils.subtract(passed, defined) + "and defined" + CollectionUtils.subtract(defined, passed));
        }
        if (!CollectionUtils.isSubCollection(passed, defined)) {
            throw new IllegalArgumentException("Passed arguments should be the subset of defined, "
                + "diff: passed" + CollectionUtils.subtract(passed, defined) + " and defined" + CollectionUtils.subtract(defined, passed));
        }
    }

    public static Map<String, String> assembleArgumentMap(String procedureName, List<String> argumentList) {
        List<String> argumentNameList = argumentsMap.get(procedureName);
        if (argumentNameList == null) {
            throw new IllegalArgumentException("No Such Procedure: " + procedureName);
        }
        if (argumentNameList.size() != argumentList.size()) {
            throw new IllegalArgumentException("Arguments Not Match of Procedure: " + procedureName);
        }
        Map<String, String> argumentMap = new HashMap<>();
        for (int i = 0; i < argumentNameList.size(); i++) {
            argumentMap.put(argumentNameList.get(i), argumentList.get(i));
        }
        return argumentMap;
    }

    private static Class runnerTestCaseClass() throws Exception {
        return Class.forName("io.cucumber.core.runner.TestCase");
    }

    private static void prepareInvocation() {
        lastResult = null;
    }

    public static void runProcedureWithArguments(String procedureName, Map<String, String> argumentMap) throws Throwable {
        checkInvoking();
        try {
            prepareInvocation();
            pushStackFrame(buildStackFrame(procedureName, argumentMap));
            printHeader(procedureName, argumentMap);
            runProcedureWithArguments0(procedureName, argumentMap);
            printFooter(procedureName);
        } finally {
            ZwsProcedureStackFrame frame = popStackFrame();
            lastResult = frame.getResult();
        }
    }

    public static void runProcedureWithArguments0(String procedureName, Map<String, String> arguments) throws Throwable {
        if (!procedureMap.containsKey(procedureName)) {
            throw new IllegalArgumentException("No Such Procedure: " + procedureName + ", procedureMap: " + procedureMap.keySet());
        }
        // expand arguments
        TestCase clone = CoreRunnerTestCaseHelper.cloneTestCaseAsInstance(procedureMap.get(procedureName));
        // run it
        Method method = ReflectionUtils.findMethod(runnerTestCaseClass(), "run", EventBus.class);
        method.setAccessible(true);
        ZwsProcedureEventBus procedureEventBus = new ZwsProcedureEventBus(ZwsProcedurePluginImpl.eventBus);
        method.invoke(clone, procedureEventBus);
        Throwable error = procedureEventBus.getError();
        if (error != null) {
            throw error;
        }
    }

    private static void checkInvoking() {
        if (/*isInvoking && */invokingLevel >= MaxStackDepth) {
            throw new IllegalStateException("Could not invoke procedure in procedure more than %d depth level".formatted(MaxStackDepth));
        }
    }

    private static void printHeader(String procedureName, Map<String, String> argumentMap) {
        int level = invokingLevel();
        logger.debug(HeaderPatterns[level], procedureName, makeArgumentsString(argumentMap));
    }

    private static String makeArgumentsString(Map<String, String> argumentMap) {
        int level = invokingLevel();
        if (MapUtils.isEmpty(argumentMap)) {
            return ZwsProcedureLoggerLayout.ProcedureDepthConverter.Slashes[level] + render("No Procedure Arguments", FG_YELLOW, BOLD);
        } else {
            return ZwsPrintTableHelper.makeTableString("Procedure Arguments", Pair.of(
                    Arrays.asList("Name", "Value"),
                    argumentMap.entrySet()
                            .stream()
                            .map(e -> {
                                Map<String, String> r = new HashMap<>();
                                r.put("Name", e.getKey());
                                r.put("Value", e.getValue());
                                return r;
                            })
                            .collect(Collectors.toList())),
                    new ZwsFixedPrefixRender(ZwsProcedureLoggerLayout.ProcedureDepthConverter.Slashes[level]));
        }
    }

    private static void printFooter(String procedureName) {
        int level = invokingLevel();
        logger.debug(FooterPatterns[level], procedureName);
    }

    /*
     * Stack
     */
    public static ZwsProcedureStackFrame buildStackFrame(String procedureName, Map<String, String> argumentMap) {
        return new ZwsProcedureStackFrame(procedureName, argumentMap);
    }

    private static void pushStackFrame(ZwsProcedureStackFrame frame) {
        logger.debug("push stack frame: {}", frame);
        stack[invokingLevel++] = frame;
        frame.setLevel(invokingLevel());
        frame.setVarBlock(ZwsVar.pushProcedureBlock());
    }

    private static ZwsProcedureStackFrame popStackFrame() {
        logger.debug("pop stack frame: {}", stack[invokingLevel() - 1]);
        ZwsVar.popProcedureBlock();
        return stack[--invokingLevel];
    }

    private static ZwsProcedureStackFrame peekStackFrame() {
        return stack[invokingLevel() - 1];
    }

    public static void setResult(Object result) {
        peekStackFrame().setResult(result);
    }

    public static Object getLastResult() {
        return lastResult;
    }

}
